/*
 * Copyright (C) 2013 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.aplink.generic.google.datetimepicker.time;

import android.animation.Keyframe;
import android.animation.ObjectAnimator;
import android.animation.PropertyValuesHolder;
import android.animation.ValueAnimator;
import android.animation.ValueAnimator.AnimatorUpdateListener;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.util.Log;
import android.view.View;

import com.aplink.generic.google.datetimepicker.Utils;
import com.aplink.sg.R;

/**
 * View to show what number is selected. This will draw a blue circle over the
 * number, with a blue line coming from the center of the main circle to the
 * edge of the blue selection.
 */
public class RadialSelectorView extends View {
    /**
     * We'll need to invalidate during the animation.
     */
    private class InvalidateUpdateListener implements AnimatorUpdateListener {
        @Override
        public void onAnimationUpdate(final ValueAnimator animation) {
            RadialSelectorView.this.invalidate();
        }
    }

    // Alpha level for the line.
    private static final int FULL_ALPHA = Utils.FULL_ALPHA;
    // Alpha level for selected circle.
    private static final int SELECTED_ALPHA = Utils.SELECTED_ALPHA;
    private static final int SELECTED_ALPHA_THEME_DARK = Utils.SELECTED_ALPHA_THEME_DARK;

    private static final String TAG = "RadialSelectorView";

    private float mAmPmCircleRadiusMultiplier;
    private float mAnimationRadiusMultiplier;

    private int mCircleRadius;
    private float mCircleRadiusMultiplier;
    private boolean mDrawValuesReady;
    private boolean mForceDrawDot;
    private boolean mHasInnerCircle;
    private float mInnerNumbersRadiusMultiplier;
    private InvalidateUpdateListener mInvalidateUpdateListener;
    private boolean mIs24HourMode;
    private boolean mIsInitialized;
    private int mLineLength;

    private float mNumbersRadiusMultiplier;
    private float mOuterNumbersRadiusMultiplier;
    private final Paint mPaint = new Paint();
    private int mSelectionAlpha;
    private int mSelectionDegrees;
    private double mSelectionRadians;
    private int mSelectionRadius;
    private float mSelectionRadiusMultiplier;

    private float mTransitionEndRadiusMultiplier;
    private float mTransitionMidRadiusMultiplier;
    private int mXCenter;

    private int mYCenter;

    public RadialSelectorView(final Context context) {
        super(context);
        this.mIsInitialized = false;
    }

    public int getDegreesFromCoords(final float pointX, final float pointY,
            final boolean forceLegal, final Boolean[] isInnerCircle) {
        if (!this.mDrawValuesReady) {
            return -1;
        }

        final double hypotenuse = Math
                .sqrt(((pointY - this.mYCenter) * (pointY - this.mYCenter))
                        + ((pointX - this.mXCenter) * (pointX - this.mXCenter)));
        // Check if we're outside the range
        if (this.mHasInnerCircle) {
            if (forceLegal) {
                // If we're told to force the coordinates to be legal, we'll set
                // the isInnerCircle
                // boolean based based off whichever number the coordinates are
                // closer to.
                final int innerNumberRadius = (int) (this.mCircleRadius * this.mInnerNumbersRadiusMultiplier);
                final int distanceToInnerNumber = (int) Math.abs(hypotenuse
                        - innerNumberRadius);
                final int outerNumberRadius = (int) (this.mCircleRadius * this.mOuterNumbersRadiusMultiplier);
                final int distanceToOuterNumber = (int) Math.abs(hypotenuse
                        - outerNumberRadius);

                isInnerCircle[0] = (distanceToInnerNumber <= distanceToOuterNumber);
            } else {
                // Otherwise, if we're close enough to either number (with the
                // space between the
                // two allotted equally), set the isInnerCircle boolean as the
                // closer one.
                // appropriately, but otherwise return -1.
                final int minAllowedHypotenuseForInnerNumber = (int) (this.mCircleRadius * this.mInnerNumbersRadiusMultiplier)
                        - this.mSelectionRadius;
                final int maxAllowedHypotenuseForOuterNumber = (int) (this.mCircleRadius * this.mOuterNumbersRadiusMultiplier)
                        + this.mSelectionRadius;
                final int halfwayHypotenusePoint = (int) (this.mCircleRadius * ((this.mOuterNumbersRadiusMultiplier + this.mInnerNumbersRadiusMultiplier) / 2));

                if ((hypotenuse >= minAllowedHypotenuseForInnerNumber)
                        && (hypotenuse <= halfwayHypotenusePoint)) {
                    isInnerCircle[0] = true;
                } else if ((hypotenuse <= maxAllowedHypotenuseForOuterNumber)
                        && (hypotenuse >= halfwayHypotenusePoint)) {
                    isInnerCircle[0] = false;
                } else {
                    return -1;
                }
            }
        } else {
            // If there's just one circle, we'll need to return -1 if:
            // we're not told to force the coordinates to be legal, and
            // the coordinates' distance to the number is within the allowed
            // distance.
            if (!forceLegal) {
                final int distanceToNumber = (int) Math.abs(hypotenuse
                        - this.mLineLength);
                // The max allowed distance will be defined as the distance from
                // the center of the
                // number to the edge of the circle.
                final int maxAllowedDistance = (int) (this.mCircleRadius * (1 - this.mNumbersRadiusMultiplier));
                if (distanceToNumber > maxAllowedDistance) {
                    return -1;
                }
            }
        }

        final float opposite = Math.abs(pointY - this.mYCenter);
        final double radians = Math.asin(opposite / hypotenuse);
        int degrees = (int) ((radians * 180) / Math.PI);

        // Now we have to translate to the correct quadrant.
        final boolean rightSide = (pointX > this.mXCenter);
        final boolean topSide = (pointY < this.mYCenter);
        if (rightSide && topSide) {
            degrees = 90 - degrees;
        } else if (rightSide && !topSide) {
            degrees = 90 + degrees;
        } else if (!rightSide && !topSide) {
            degrees = 270 - degrees;
        } else if (!rightSide && topSide) {
            degrees = 270 + degrees;
        }
        return degrees;
    }

    public ObjectAnimator getDisappearAnimator() {
        if (!this.mIsInitialized || !this.mDrawValuesReady) {
            Log.e(RadialSelectorView.TAG,
                    "RadialSelectorView was not ready for animation.");
            return null;
        }

        Keyframe kf0, kf1, kf2;
        final float midwayPoint = 0.2f;
        final int duration = 500;

        kf0 = Keyframe.ofFloat(0f, 1);
        kf1 = Keyframe
                .ofFloat(midwayPoint, this.mTransitionMidRadiusMultiplier);
        kf2 = Keyframe.ofFloat(1f, this.mTransitionEndRadiusMultiplier);
        final PropertyValuesHolder radiusDisappear = PropertyValuesHolder
                .ofKeyframe("animationRadiusMultiplier", kf0, kf1, kf2);

        kf0 = Keyframe.ofFloat(0f, 1f);
        kf1 = Keyframe.ofFloat(1f, 0f);
        final PropertyValuesHolder fadeOut = PropertyValuesHolder.ofKeyframe(
                "alpha", kf0, kf1);

        final ObjectAnimator disappearAnimator = ObjectAnimator
                .ofPropertyValuesHolder(this, radiusDisappear, fadeOut)
                .setDuration(duration);
        disappearAnimator.addUpdateListener(this.mInvalidateUpdateListener);

        return disappearAnimator;
    }

    public ObjectAnimator getReappearAnimator() {
        if (!this.mIsInitialized || !this.mDrawValuesReady) {
            Log.e(RadialSelectorView.TAG,
                    "RadialSelectorView was not ready for animation.");
            return null;
        }

        Keyframe kf0, kf1, kf2, kf3;
        float midwayPoint = 0.2f;
        final int duration = 500;

        // The time points are half of what they would normally be, because this
        // animation is
        // staggered against the disappear so they happen seamlessly. The
        // reappear starts
        // halfway into the disappear.
        final float delayMultiplier = 0.25f;
        final float transitionDurationMultiplier = 1f;
        final float totalDurationMultiplier = transitionDurationMultiplier
                + delayMultiplier;
        final int totalDuration = (int) (duration * totalDurationMultiplier);
        final float delayPoint = (delayMultiplier * duration) / totalDuration;
        midwayPoint = 1 - (midwayPoint * (1 - delayPoint));

        kf0 = Keyframe.ofFloat(0f, this.mTransitionEndRadiusMultiplier);
        kf1 = Keyframe.ofFloat(delayPoint, this.mTransitionEndRadiusMultiplier);
        kf2 = Keyframe
                .ofFloat(midwayPoint, this.mTransitionMidRadiusMultiplier);
        kf3 = Keyframe.ofFloat(1f, 1);
        final PropertyValuesHolder radiusReappear = PropertyValuesHolder
                .ofKeyframe("animationRadiusMultiplier", kf0, kf1, kf2, kf3);

        kf0 = Keyframe.ofFloat(0f, 0f);
        kf1 = Keyframe.ofFloat(delayPoint, 0f);
        kf2 = Keyframe.ofFloat(1f, 1f);
        final PropertyValuesHolder fadeIn = PropertyValuesHolder.ofKeyframe(
                "alpha", kf0, kf1, kf2);

        final ObjectAnimator reappearAnimator = ObjectAnimator
                .ofPropertyValuesHolder(this, radiusReappear, fadeIn)
                .setDuration(totalDuration);
        reappearAnimator.addUpdateListener(this.mInvalidateUpdateListener);
        return reappearAnimator;
    }

    /**
     * Allows for smoother animations.
     */
    @Override
    public boolean hasOverlappingRendering() {
        return false;
    }

    /**
     * Initialize this selector with the state of the picker.
     * 
     * @param context
     *            Current context.
     * @param is24HourMode
     *            Whether the selector is in 24-hour mode, which will tell us
     *            whether the circle's center is moved up slightly to make room
     *            for the AM/PM circles.
     * @param hasInnerCircle
     *            Whether we have both an inner and an outer circle of numbers
     *            that may be selected. Should be true for 24-hour mode in the
     *            hours circle.
     * @param disappearsOut
     *            Whether the numbers' animation will have them disappearing out
     *            or disappearing in.
     * @param selectionDegrees
     *            The initial degrees to be selected.
     * @param isInnerCircle
     *            Whether the initial selection is in the inner or outer circle.
     *            Will be ignored when hasInnerCircle is false.
     */
    public void initialize(final Context context, final boolean is24HourMode,
            final boolean hasInnerCircle, final boolean disappearsOut,
            final int selectionDegrees, final boolean isInnerCircle) {
        if (this.mIsInitialized) {
            Log.e(RadialSelectorView.TAG,
                    "This RadialSelectorView may only be initialized once.");
            return;
        }

        final Resources res = context.getResources();

        final int blue = res.getColor(R.color.blue);
        this.mPaint.setColor(blue);
        this.mPaint.setAntiAlias(true);
        this.mSelectionAlpha = RadialSelectorView.SELECTED_ALPHA;

        // Calculate values for the circle radius size.
        this.mIs24HourMode = is24HourMode;
        if (is24HourMode) {
            this.mCircleRadiusMultiplier = Float.parseFloat(res
                    .getString(R.string.circle_radius_multiplier_24HourMode));
        } else {
            this.mCircleRadiusMultiplier = Float.parseFloat(res
                    .getString(R.string.circle_radius_multiplier));
            this.mAmPmCircleRadiusMultiplier = Float.parseFloat(res
                    .getString(R.string.ampm_circle_radius_multiplier));
        }

        // Calculate values for the radius size(s) of the numbers circle(s).
        this.mHasInnerCircle = hasInnerCircle;
        if (hasInnerCircle) {
            this.mInnerNumbersRadiusMultiplier = Float.parseFloat(res
                    .getString(R.string.numbers_radius_multiplier_inner));
            this.mOuterNumbersRadiusMultiplier = Float.parseFloat(res
                    .getString(R.string.numbers_radius_multiplier_outer));
        } else {
            this.mNumbersRadiusMultiplier = Float.parseFloat(res
                    .getString(R.string.numbers_radius_multiplier_normal));
        }
        this.mSelectionRadiusMultiplier = Float.parseFloat(res
                .getString(R.string.selection_radius_multiplier));

        // Calculate values for the transition mid-way states.
        this.mAnimationRadiusMultiplier = 1;
        this.mTransitionMidRadiusMultiplier = 1f + (0.05f * (disappearsOut ? -1
                : 1));
        this.mTransitionEndRadiusMultiplier = 1f + (0.3f * (disappearsOut ? 1
                : -1));
        this.mInvalidateUpdateListener = new InvalidateUpdateListener();

        setSelection(selectionDegrees, isInnerCircle, false);
        this.mIsInitialized = true;
    }

    @Override
    public void onDraw(final Canvas canvas) {
        final int viewWidth = getWidth();
        if ((viewWidth == 0) || !this.mIsInitialized) {
            return;
        }

        if (!this.mDrawValuesReady) {
            this.mXCenter = getWidth() / 2;
            this.mYCenter = getHeight() / 2;
            this.mCircleRadius = (int) (Math.min(this.mXCenter, this.mYCenter) * this.mCircleRadiusMultiplier);

            if (!this.mIs24HourMode) {
                // We'll need to draw the AM/PM circles, so the main circle will
                // need to have
                // a slightly higher center. To keep the entire view centered
                // vertically, we'll
                // have to push it up by half the radius of the AM/PM circles.
                final int amPmCircleRadius = (int) (this.mCircleRadius * this.mAmPmCircleRadiusMultiplier);
                this.mYCenter -= amPmCircleRadius / 2;
            }

            this.mSelectionRadius = (int) (this.mCircleRadius * this.mSelectionRadiusMultiplier);

            this.mDrawValuesReady = true;
        }

        // Calculate the current radius at which to place the selection circle.
        this.mLineLength = (int) (this.mCircleRadius
                * this.mNumbersRadiusMultiplier * this.mAnimationRadiusMultiplier);
        int pointX = this.mXCenter
                + (int) (this.mLineLength * Math.sin(this.mSelectionRadians));
        int pointY = this.mYCenter
                - (int) (this.mLineLength * Math.cos(this.mSelectionRadians));

        // Draw the selection circle.
        this.mPaint.setAlpha(this.mSelectionAlpha);
        canvas.drawCircle(pointX, pointY, this.mSelectionRadius, this.mPaint);

        if (this.mForceDrawDot | ((this.mSelectionDegrees % 30) != 0)) {
            // We're not on a direct tick (or we've been told to draw the dot
            // anyway).
            this.mPaint.setAlpha(RadialSelectorView.FULL_ALPHA);
            canvas.drawCircle(pointX, pointY,
                    ((this.mSelectionRadius * 2) / 7), this.mPaint);
        } else {
            // We're not drawing the dot, so shorten the line to only go as far
            // as the edge of the
            // selection circle.
            int lineLength = this.mLineLength;
            lineLength -= this.mSelectionRadius;
            pointX = this.mXCenter
                    + (int) (lineLength * Math.sin(this.mSelectionRadians));
            pointY = this.mYCenter
                    - (int) (lineLength * Math.cos(this.mSelectionRadians));
        }

        // Draw the line from the center of the circle.
        this.mPaint.setAlpha(255);
        this.mPaint.setStrokeWidth(1);
        canvas.drawLine(this.mXCenter, this.mYCenter, pointX, pointY,
                this.mPaint);
    }

    /**
     * Set the multiplier for the radius. Will be used during animations to move
     * in/out.
     */
    public void setAnimationRadiusMultiplier(
            final float animationRadiusMultiplier) {
        this.mAnimationRadiusMultiplier = animationRadiusMultiplier;
    }

    /**
     * Set the selection.
     * 
     * @param selectionDegrees
     *            The degrees to be selected.
     * @param isInnerCircle
     *            Whether the selection should be in the inner circle or outer.
     *            Will be ignored if hasInnerCircle was initialized to false.
     * @param forceDrawDot
     *            Whether to force the dot in the center of the selection circle
     *            to be drawn. If false, the dot will be drawn only when the
     *            degrees is not a multiple of 30, i.e. the selection is not on
     *            a visible number.
     */
    public void setSelection(final int selectionDegrees,
            final boolean isInnerCircle, final boolean forceDrawDot) {
        this.mSelectionDegrees = selectionDegrees;
        this.mSelectionRadians = (selectionDegrees * Math.PI) / 180;
        this.mForceDrawDot = forceDrawDot;

        if (this.mHasInnerCircle) {
            if (isInnerCircle) {
                this.mNumbersRadiusMultiplier = this.mInnerNumbersRadiusMultiplier;
            } else {
                this.mNumbersRadiusMultiplier = this.mOuterNumbersRadiusMultiplier;
            }
        }
    }

    /* package */void setTheme(final Context context, final boolean themeDark) {
        final Resources res = context.getResources();
        int color;
        if (themeDark) {
            color = res.getColor(R.color.red);
            this.mSelectionAlpha = RadialSelectorView.SELECTED_ALPHA_THEME_DARK;
        } else {
            color = res.getColor(R.color.blue);
            this.mSelectionAlpha = RadialSelectorView.SELECTED_ALPHA;
        }
        this.mPaint.setColor(color);
    }
}
